package com.sailing.xjpb.web.base.api;

import lombok.Data;
import org.apache.commons.lang3.StringUtils;
import org.springframework.lang.Nullable;
import org.springframework.web.servlet.mvc.condition.RequestCondition;

import javax.servlet.http.HttpServletRequest;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * 接口版本控制器
 * 主要为了不同版本的接口请求可以分发到相同路由且不同版本的接口
 * 如果@ApiVersion注解标注在类上，则该类的所有方法都是一个版本，如果标注在方法上，则该方法是单独一个版本，方法版本优先于类版本
 * 例如：请求地址1：${contextPath}/xxx/yyy/V1，请求地址2：${contextPath}/xxx/yyy/V2，我们只需要在${contextPath}/xxx/yyy接口上指定@ApiVersion(1)
 * 或@ApiVersion(2)就能访问到指定的接口
 * 版本号优先级：url>header>param
 * 需注意：如果是用url校验，则为了适配没有版本号的url，需要类上的@RequestMapping里写两种映射，类似：@RequestMapping({"/test/{version}","/test"})
 * 如果请求版本号大于接口版本号或者是错误的版本号或者没有版本号，能匹配到最新版本的接口，如果请求版本号等于接口版本号，则匹配到改版本号的接口，如果版本号小于所有历史版本接口的版本号，则404
 *
 * @author FanHaiChao
 * createDate 2024/6/25 10:56
 */
@Data
public class ApiVersionCondition implements RequestCondition<ApiVersionCondition> {

    /**
     * url版本号正则表达式
     */
    private static final Pattern VERSION_PREFIX_PATTERN = Pattern.compile(".*V(\\d+).*");

    /**
     * 版本号参数key
     */
    private static final String API_VERSION_KEY = "api-version";

    /**
     * 接口版本号
     */
    private final int apiVersion;

    public ApiVersionCondition(int apiVersion) {
        this.apiVersion = apiVersion;
    }

    /**
     * 一个http接口上有多个条件规则时，用于合并，比如controller和method上的requestMapping映射合并
     *
     * @param other 其他与之结合的条件
     * @return 作为组合两个条件实例的结果的请求条件实例
     */
    @Override
    public ApiVersionCondition combine(ApiVersionCondition other) {
        return new ApiVersionCondition(other.getApiVersion());
    }

    /**
     * 检查条件是否与返回可能为当前请求创建的新实例的请求匹配。例如，具有多个URL模式的条件可能只返回具有与请求匹配的模式的新实例。
     * 先检查url中是否有版本控制，再检查header，最后检查param中是否有版本控制
     * 需注意：如果是用url校验，则为了适配没有版本号的url，需要类上的@RequestMapping里写两种映射，类似：@RequestMapping({"/test/{version}","/test"})
     * 如果请求版本号大于接口版本号或者是错误的版本号或者没有版本号，能匹配到最新版本的接口，如果请求版本号等于接口版本号，则匹配到改版本号的接口，如果版本号小于所有历史版本接口的版本号，则404
     *
     * @param request servlet请求对象
     * @return 匹配的话为条件实例，不匹配返回为null。
     */
    @Override
    @Nullable
    public ApiVersionCondition getMatchingCondition(HttpServletRequest request) {
        Matcher urlMatch = VERSION_PREFIX_PATTERN.matcher(request.getRequestURI());
        if (urlMatch.find()) {
            int version = Integer.parseInt(urlMatch.group(1));
            //如果url带版本号,则判断版本是否符合，返回符合的
            if (version >= this.apiVersion) {
                return this;
            }
        } else {
            //如果url不带版本号，则获取header或者param的版本号，如果获取不到，则都返回，取最新的，如果获取到了，按版本号返回
            String versionStr = StringUtils.isNotBlank(request.getHeader(API_VERSION_KEY)) ?
                    request.getHeader(API_VERSION_KEY) : request.getParameter(API_VERSION_KEY);
            if (StringUtils.isNotBlank(versionStr)) {
                Matcher match = VERSION_PREFIX_PATTERN.matcher(versionStr);
                if (match.find()) {
                    int version = Integer.parseInt(match.group(1));
                    if (version >= this.apiVersion) {
                        return this;
                    }
                }
            }
        }
        return null;
    }

    /**
     * 将此条件与特定请求上下文中的另一个条件进行比较。用于多个匹配的条件对象，一般按照比较结果获取第一个，这里比较规则：按照版本号倒序
     *
     * @param other   另一个条件对象
     * @param request servlet请求对象
     * @return 比较结果
     */
    @Override
    public int compareTo(ApiVersionCondition other, HttpServletRequest request) {
        return other.getApiVersion() - this.apiVersion;
    }

    /**
     * 获取当前条件的版本号
     *
     * @return 当前条件的版本号
     */
    private int getApiVersion() {
        return this.apiVersion;
    }
}
